#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# this lesson is based on python 3

# ====CALL A FUNCTION====
print(abs(-12)) # print 12. python has many build-in functions. abs() get the absolute value

# variable type changing functions are very useful
print(int(12.23)) # print 12
print(str(12.23)) # print '12.23'
print(bool(12.23)) # print True
print(bool('')) # print False
# empty string, list, tuple and dict are seen as False
print(float('12.23')) # print 12.23

a = abs # you can assign a function to a new variable name, or say alias.
print(a(-12)) # print 12.


# ====DEFINE A FUNCTION====
def my_abs(x):
    if x > 0:
        return x
    else:
        return -x
# if you have no return calling in a function or provide no value to return, it will return None
# now you define my_abs() in pyFunc.py, you can use `from pyFunc import my_abs` in the same directory

def nof():
    pass # pass serve as a placeholder. if you do not write anything here, an empty function cannot run

def my_abs2(x):
    if not isinstance(x, (int, float)): # isinstance() check the type of instance
        raise TypeError('bad operand type') # raise error prompt. this make the function more reliable
    if x >= 0:
        return x
    else:
        return -x

import math # import built-in package

def move(x, y, step, angle=0): # prefine an argument if not provided
    nx = x + step * math.cos(angle)
    ny = y - step * math.sin(angle)
    return nx, ny # function can return multiple values

x, y = move(100, 100, 60, math.pi / 6)
print(x, y) # print 151.96152422706632 70.0

r = move(100, 100, 60, math.pi / 6)
print(r) # print (151.96152422706632 70.0)
# when function return multiple values, it actually returns a tuple
# And tuple can be auto-assigned to multiple variables

# ====ARGUMENTS====
def default(a1, a2=0, a3=7): # arguments with default value must be placed behind others
    pass

default(2, a3=3) # you need to name the arguments if you want to assign the value unorderly

def add_end(L=[]): # [] is a mutable object, so it is dangerous to be used as a default value
    L.append('END')
    return L
# if you run add_end() several times, you will get ('END','END',...)

def add_end2(L=None): # None is unmutable
    if L is None:
        L = []
    L.append('END')
    return L

def calc(*args): # *args means this function can receive multiple undefined number of arguments
    sum = 0
    for n in args:
        sum = sum + n * n
    return sum

calc(1, 2, 3) # print 14
calc() # print 0. you can pass no arguments, too
nlist = [3, 4] 
calc(*nlist) # print 25. you can pass a list or tuple like this into *args

def person(name, age, **kw): # key word parameter can receive multiple key-value pair and pack them as a dict
    print('name:', name, 'age:', age, 'other:', kw)

person('Bill', 12)
person('Alice', 88, hobby='singing', hometown='Cliffway') 
extra = {'hobby': 'galgame', 'hometown': 'Node08'}
person('Celia', 3, **extra) # a dict can be passed into **kw like this

def person2(name, age, *, city, job): 
    print(name, age, city, job)
# arguments behind * are named key word parameter. they are just like **kw, except the keys are already defined
person2('Jack', 24, city='Beijing', job='Engineer')

def person3(name, age, *args, city, job): # arguments behind *args automatically become named key word parameter
    print(name, age, args, city, job)
person3('Jack', 24, 'why?', city='Beijing', job='Engineer')

def person4(name, age, *args, city='Paris', job): # named key word parameter can also have default value
    print(name, age, args, city, job)

def f1(a, b, c=0, *args, name, **kw): # you can define a function with all 5 kinds parameters
    print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'name =', name, 'kw =', kw)
# if you do this, remember the order to claim them, which cannot be changed

args = (1, 2, 3, 4)
kw = {'d': 99, 'name': '#'}
f1(*args, **kw) # pass in a list and a dict actually can work for any function


# ====RECURSIVE====
"""
a function invoke itself in its definition is called a recursive function.
theorically, all recursive functions can be written in loops, but that make the logic murky.
a major error we need to avoid when using recursive is stack overflow.
"""
def fact(n): # a function do the factorial (n!)
    if n==1:
        return 1
    return n * fact(n - 1)
# if we invoke fact(1000), too many layers of self-invoke will cause stack overflow

"""
we can use tail recursive to avoid stack overflow.
tail recursive means, in the return line, invoke itself and avoid any math operation.
"""
def fact2(n):
    return fact_iter(n, 1)

def fact_iter(num, product):
    if num == 1:
        return product
    return fact_iter(num - 1, num * product) 
    # here the math opertion is before the invoke of function, so it's OK
"""
however, most program intepreters, include python's, do not optimize for tail recursive.
so even if you run fact2(1000) in python, it still cause stack overflow
"""
